﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Engine.Sgml.Parser;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace Engine.Sgml.Decoding {
    internal sealed class Detector {
        private static Regex contentTypeRegex = new Regex(@"((?'type'[!#-'*-+\-.0-9A-Z^-z|~]+)\s*\/\s*(?'subtype'[!#-'*-+\-.0-9A-Z^-z|~]+))+(?'parameter'\s*;\s*(?'attribute'[!#-'*-+\-.0-9A-Z^-z|~]+)\s*=\s*(?'value'[!#-'*-+\-.0-9A-Z^-z|~]+))?", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant);

        public static Encoding DetectEncoding(string contentType) {
            Guard.ArgumentNotNull(contentType, "contentType");
            Match match = contentTypeRegex.Match(contentType);
            if (match.Success && match.Groups["parameter"].Success && string.Equals(match.Groups["attribute"].Value, "charset", StringComparison.OrdinalIgnoreCase)) {
                try {
                    return Encoding.GetEncoding(match.Groups["value"].Value);
                    }
                catch (ArgumentException) {
                    // Ignore this exception
                    // Name is not a valid code page name or
                    // The code page indicated by name is
                    // not supported by the underlying platform.
                    }
                }
            return null;
            }

        public static SgmlDocumentType DetectDocumentType(string contentType) {
            Debug.Assert(contentType != null);
            Guard.ArgumentNotNull(contentType, "contentType");
            // media-type     = type "/" subtype *( ";" parameter )
            // type           = token
            // subtype        = token
            // parameter      = token "=" value
            // value := token / quoted-string
            // token = [!#-'*-+\-.0-9A-Z^-z|~]+
            Match match = contentTypeRegex.Match(contentType);
            if (match.Success) {
                do {
                    string type = match.Groups["type"].Value;
                    if (string.Equals(type, "text", StringComparison.OrdinalIgnoreCase)) {
                        string subtype = match.Groups["subtype"].Value;
                        if (string.Equals(subtype, "html", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Html;
                        if (string.Equals(subtype, "xml", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Xml;
                        }
                    else if (string.Equals(type, "application", StringComparison.OrdinalIgnoreCase)) {
                        string subtype = match.Groups["subtype"].Value;
                        if (string.Equals(subtype, "xml", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Xml;
                        if (string.Equals(subtype, "xml+xhtml", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Html;
                        if (string.Equals(subtype, "mathml+xml", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Xml;
                        if (string.Equals(subtype, "xslt+xml", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Xml;
                        if (string.Equals(subtype, "rdf+xml", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Xml;
                        if (string.Equals(subtype, "svg+xml", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Xml;
                        if (string.Equals(subtype, "xml-dtd", StringComparison.OrdinalIgnoreCase))
                            return SgmlDocumentType.Xml;
                        }
                    match = match.NextMatch();
                    } while (match.Success);
                }
            return SgmlDocumentType.Unknown;
            }

        public static Encoding DetectEncodingFromXmlDeclaration(SgmlStreamReader reader, SElement root, Context context) {
            Guard.ArgumentNotNull(reader, "reader");
            Guard.ArgumentNotNull(root, "root");
            Guard.ArgumentNotNull(context, "context");
            reader.ReadChar();
            if (reader.EndOfStream)
                return null;
            reader.SkipWhitespace();
            context.SetLineInfo(reader);
            SgmlNodeType type = SgmlParser.DetectNodeType(reader, root);
            switch (type) {
                case SgmlNodeType.ProcessingInstruction:
                    IElementReader piReader = context.Readers[SgmlNodeType.ProcessingInstruction];
                    SElement pi = piReader.Read(reader, root, context);
                    string encoding = pi.Attribute("encoding");
                    if (encoding != null)
                        try {
                            return Encoding.GetEncoding(encoding);
                            }
                        catch (ArgumentException) {
                            // Name is not a valid code page name or
                            // The code page indicated by name is
                            // not supported by the underlying platform.
                            }
                    root.Nodes.Pop();
                    break;
                case SgmlNodeType.None:
                    return null;
                default:
                    IElementReader er;
                    if (context.Readers.TryGetValue(type, out er)) {
                        er.Read(reader, root, context);
                        root.Nodes.Pop();
                        }
                    else
                        throw new InvalidOperationException();
                    break;
                }
            return null;
            }

        public static Encoding DetectEncodingFromMetaTag(SgmlStreamReader reader, SElement root, Context context) {
            Guard.ArgumentNotNull(reader, "reader");
            Guard.ArgumentNotNull(root, "root");
            Guard.ArgumentNotNull(context, "context");
            reader.SkipWhitespace();
            if (reader.EndOfStream)
                return null;
            IElementReader elementReader = context.Readers[SgmlNodeType.Element];
            while (!reader.EndOfStream) {
                context.SetLineInfo(reader);
                SgmlNodeType type = SgmlParser.DetectNodeType(reader, root);
                switch (type) {
                    case SgmlNodeType.Element:
                        SElement meta = elementReader.Read(reader, root, context);
                        if (meta.LocalName.Equals("meta", StringComparison.OrdinalIgnoreCase)) {
                            string charset = meta.Attribute("charset");
                            if (charset != null)
                                try {
                                    return Encoding.GetEncoding(charset);
                                    }
                                catch (ArgumentException) {
                                    // Name is not a valid code page name or
                                    // The code page indicated by name is
                                    // not supported by the underlying platform.
                                    }
                            string httpequiv = meta.Attribute("http-equiv");
                            if (httpequiv != null && httpequiv.Equals("content-type", StringComparison.OrdinalIgnoreCase)) {
                                string content = meta.Attribute("content");
                                if (content != null) {
                                    Encoding encoding = DetectEncoding(content);
                                    if (encoding != null)
                                        return encoding;
                                    }
                                }
                            }
                        root.Nodes.Pop();
                        break;
                    case SgmlNodeType.None:
                        return null;
                    default:
                        IElementReader r;
                        if (context.Readers.TryGetValue(type, out r)) {
                            r.Read(reader, root, context);
                            root.Nodes.Pop();
                            }
                        else
                            throw new InvalidOperationException();
                        break;
                    }
                }
            return null;
            }

        public static Decoder DetectDecoderFromOrderByteMarks(byte[] buffer, ref int index, int length) {
            if (4 <= (length - index)) {
                uint w = (uint)buffer[index + 0] << 24 | (uint)buffer[index + 1] << 16 | (uint)buffer[index + 2] << 8 | (uint)buffer[index + 3];
                // See if it's a 4-byte encoding
                switch (w) {
                    case 0xfefffeff:
                        index += 4;
                        return new Ucs4DecoderBigEngian();
                    case 0xfffefffe:
                        index += 4;
                        return new Ucs4DecoderLittleEndian();
                    case 0x3c000000:
                        goto case 0xfefffeff;
                    case 0x0000003c:
                        goto case 0xfffefffe;
                    }
                w >>= 8;
                if (w == 0xefbbbf) {
                    index += 3;
                    return Encoding.UTF8.GetDecoder();
                    }
                w >>= 8;
                switch (w) {
                    case 0xfeff:
                        index += 2;
                        return UnicodeEncoding.BigEndianUnicode.GetDecoder();
                    case 0xfffe:
                        index += 2;
                        return new UnicodeEncoding(false, false).GetDecoder();
                    case 0x3c00:
                        goto case 0xfeff;
                    case 0x003c:
                        goto case 0xfffe;
                    }
                }
            return null;
            }
        }
    }
